package team3;

import java.util.*;
import project2.AI;

/** Represents the Board in a game of Reversi.
 * <p>
 * Provides functionality for determining legal moves for a player,
 * executing a move including flipping of enemy tiles, determining if the
 * game has ended, and who has won.
 * <p>
 * For all intents and purposes, BoardState should be considered as an
 * immutable object.
 * <p>
 * BoardState is only guaranteed work when the state is an 8x8 matrix.
 * @author Brett
 */
public class BoardState
{
    /** The official representation of an invalid move/pass.
     * <p>
     * All AI implementations should refer to this property
     * when a move is not possible.
     */
    static int[] INVALID_MOVE=new int[]{-1, -1};

    private AI.Player[][] state;
    private TreeMap<int[], List<int[]>> availableBlackMoves;
    private TreeMap<int[], List<int[]>> availableWhiteMoves;

    /**Constructor for BoardState.
     * <p>
     * It sets the state of the Board and it calculates available move
     * positions and their corresponding flips.
     * @param state The state to set the board to.
     */
	public BoardState(AI.Player[][] state)
	{
		this.state = state;

        //Find all of the available moves and store them
        //This is done to avoid going through algorithms more than once
        this.availableBlackMoves = new TreeMap<int[], List<int[]>>(new MoveComparator());
        this.availableWhiteMoves = new TreeMap<int[], List<int[]>>(new MoveComparator());
        ArrayList<int[]> emptyTiles = new ArrayList<int[]>();

        //Find all the empty tiles
        for (int i = 0; i < 8; i++)
            for (int j = 0; j < 8; j++)
                if (state[i][j] == AI.Player.EMPTY)
                    emptyTiles.add(new int[] { i, j });

        //See if the empty tile is an available move
        for (int[] empty : emptyTiles)
        {
            List<int[]> whiteFlips = getFlips(AI.Player.WHITE, empty);
            List<int[]> blackFlips = getFlips(AI.Player.BLACK, empty);
            //Checks to see if the move will result in flips
            if (!whiteFlips.isEmpty())
                availableWhiteMoves.put(empty, whiteFlips);
            if (!blackFlips.isEmpty())
                availableBlackMoves.put(empty, blackFlips);
        }
	}

    /**Makes the given move on a new board state if it is a valid move.
     * If it is an invalid move, then it is considered a 'pass'.
     * <p>
     * Does not modify the parent board state.
     * @param player The Player who is performing the move.
     * @param move The move that the Player is performing.
     * @return The new BoardState.
     */
	public BoardState makeMove(AI.Player player, int[] move)
	{
        //create a copy of the state, then modify the new state according to game rules
        AI.Player[][] newState = cloneState();

        if (moveIsValid(player, move))
        {
            //Perform move
            newState[move[0]][move[1]] = player;

            //Perform flips
            List<int[]> flips;
            if (player == AI.Player.BLACK)
                flips = availableBlackMoves.get(move);
            else //player == AI.Player.WHITE
                flips = availableWhiteMoves.get(move);
            for (int[] flip : flips)
                newState[flip[0]][flip[1]] = player;
        }

		return new BoardState(newState);
	}

    /**Find all moves that can be made by the given player.
     * @param player The Player that wants available moves.
     * @return List of moves in form of int[]. If no moves are available, will be empty.
     */
	public List<int[]> getAvailableMoves(AI.Player player)
	{
        if (player == AI.Player.BLACK)
            return new ArrayList<int[]>(availableBlackMoves.keySet());
        return new ArrayList<int[]>(availableWhiteMoves.keySet());
	}

    /** Returns a clone of the game state. Any modifications to this state won't
     * effect the internal representation of the board.
     * @return A copy of the Board's state
     */
    public AI.Player[][] cloneState()
    {
		AI.Player[][] newState = new AI.Player[this.state.length][];
		for(int row=0; row<newState.length; row++)
		{
			newState[row]=new AI.Player[this.state[row].length];
			for(int col=0; col<newState[row].length; col++)
				newState[row][col] = state[row][col];
		}
		return newState;
    }

    /**Returns the internal state of the Board.
     * <p>
     * NOTE!!! Changes made to this state will propogate to the internal state.
     * This should only be used in read-only performance constrained situations
     * (evaluating goodness)
     * @return Board state
     */
	public AI.Player[][] getState()
	{
        return state;
	}

    /**Static method that gives a new starting board.
     * @return The starting board state.
     */
    public static BoardState getStartingBoard()
    {
        //Sets up a new board
        AI.Player[][] startState = new AI.Player[8][8];
        for (int i = 0; i < 8; i++)
            for (int j = 0; j < 8; j++)
                startState[i][j] = AI.Player.EMPTY;
        startState[3][3] = AI.Player.WHITE;
        startState[4][4] = AI.Player.WHITE;
        startState[4][3] = AI.Player.BLACK;
        startState[3][4] = AI.Player.BLACK;

        return new BoardState(startState);
    }

    /**Determines whether or not the game is complete.
     * <p>
     * Determines if any moves can be made by either party.
     * If no moves are available for either player, the game is over.
     * @return Whether or not the game is finished.
     */
    public boolean isGameFinished()
    {
		int availableMoves = availableBlackMoves.size() + availableWhiteMoves.size();
		return availableMoves == 0;
    }

    /**Counts the board's tiles and returns the Player who has more.
     * @return The Player with more tiles, or Player.EMPTY if tied
     */
    public AI.Player getWinner()
    {
        int black = 0;
        int white = 0;
        //Find black and white tiles
        for (int i = 0; i < 8; i++)
        {
            for (int j = 0; j < 8; j++)
            {
                if (state[i][j] == AI.Player.WHITE)
                    white++;
                else if (state[i][j] == AI.Player.BLACK)
                    black++;
            }
        }
        //See who has more tiles and return
        if (black > white)
            return AI.Player.BLACK;
        else if (white > black)
            return AI.Player.WHITE;
        else
            return AI.Player.EMPTY;
    }

    /** Determines if the given move can be made by the player.
     * @param player The Player who is making the move.
     * @param move The move that the player is making.
     * @return Whether or not the move is valid.
     */
    public boolean moveIsValid(AI.Player player, int[] move)
    {
        if(move==null)
            return false;
        if (player == AI.Player.BLACK && availableBlackMoves.containsKey(move))
            return true;
        else if (player == AI.Player.WHITE & availableWhiteMoves.containsKey(move))
            return true;
        return false;
    }

    /**Finds all tiles that would be flipped if the given player
     * made the given move.
     * @param player The player performing the move.
     * @param move The move that the player is performing. This move need not be
     * valid according to game rules, though must be an empty tile on the game board.
     * @return A list of the flips that result from the given move.
     */
    private List<int[]> getFlips(AI.Player player, int[] move)
    {
        ArrayList<int[]> flips = new ArrayList<int[]>();

        //Find which color we are looking for
        AI.Player enemy = (player == AI.Player.BLACK) ? AI.Player.WHITE : AI.Player.BLACK;

        //Loop that looks in all 8 directions for flips
        for (int rowInc = -1; rowInc <= 1; rowInc++)
        {
            for (int colInc = -1; colInc <= 1; colInc++)
            {
                //If we have both increments at 0, then go to the next iteration of the loop
                if (rowInc == colInc && rowInc == 0)
                    continue;
                //flip tells if we ended a formation to flip
                boolean flip = false;
                //found tells if we found tiles that can be flipped
                boolean found = false;

                //Initially move away from the move by the increment
                int row = move[0] + rowInc;
                int col = move[1] + colInc;
                //Stay in bounds of the board
                while (-1 < col && col < 8 && -1 < row && row < 8)
                {
                    //If we find the Player color we are looking for, continue looking
                    if (state[row][col] == enemy)
                    {
                        row += rowInc;
                        col += colInc;
                        found = true;
                        continue;
                    }
                    //If we come across an Empty tile, then we end loop for this direction and don't do any flips
                    else if (state[row][col] == AI.Player.EMPTY)
                        break;
                    //Found a stone of our color, we can flip; break out of loop
                    else if (found)
                    {
                        flip = true;
                        //move to last stone that was flipped
                        row -= rowInc;
                        col -= colInc;
                        break;
                    }
                    //First stone found was of players color, nothing to flip
                    else
                        break;
                }
                //Add flips
                if (flip)
                {
                    //Backtrack towards the original move and add flips
                    while (!(row == move[0] && col == move[1]))
                    {
                        flips.add(new int[] { row, col });
                        row -= rowInc;
                        col -= colInc;
                    }
                }
            }
        }

        return flips;
    }
}